/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.devspan.tokbox;
// This file has been written by Mario Scholser from Vostu.
// We would like to thank him for his contribution and for sharing the file with us,
// and clarify that the code is not being maintained by TokBox Inc.
import com.sdicons.json.model.JSONBoolean;
import com.sdicons.json.model.JSONInteger;
import com.sdicons.json.model.JSONObject;
import com.sdicons.json.model.JSONString;
import com.sdicons.json.parser.JSONParser;
import com.devspan.tokbox.domain.TokBoxAccount;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;

public class TokBox {
    String apiKey;
    String apiSecret;
    String apiUrl;
    
    public static final String TOKBOX_API_URL = "http://api.tokbox.com/api.php";
    
    private static final String HEX_DIGITS = "0123456789abcdef";
    private static final String EMPTY_PARAM = "";
    
    public static final int AUTHENTICATION_FAILED = 1;
    public static final int PARTNERID_INVALID = 2;
    public static final int PERMISSION_DENIED = 4;
    public static final int FIRSTNAME_INVALID = 8;
    public static final int LASTNAME_INVALID = 16;
    public static final int USERNAME_INVALID = 32;
    public static final int PASSWORD_INVALID = 64;
    public static final int EMAIL_INVALID = 128;
    public static final int USERNAME_TAKEN = 256;
    public static final int EMAIL_TAKEN = 512;
    public static final int CALL_INVALID = 1024;
    
    public static final int INTERNAL_ERROR = -123;
    
    /**
     * Cache registered accounts and info retrieved for this account during runtime.
     * Info will be rebuilt as needed after a restart.
     */
    private Map<String, TokBoxAccount> accountCache = new HashMap<String, TokBoxAccount>();

    /**
     * 
     * @param apiKey API Key provided by TokBox; should be a short number.
     * @param secret An hash provided by TokBox to be used with API key.
     * @param url The URL to post requests to.
     */
    public TokBox(String apiKey, String secret, String url) {
        this.apiKey = apiKey;
        this.apiSecret = secret;
        this.apiUrl = url;
    }

    private String toHexString(byte[] v) {
        StringBuffer sb = new StringBuffer(v.length * 2);
        for (int i = 0; i < v.length; i++) {
            int b = v[i] & 0xFF;
            sb.append(HEX_DIGITS.charAt(b >>> 4)).append(HEX_DIGITS.charAt(b & 0xF));
        }
        return sb.toString();
    }

    /**
     * Get a hex encoded MD5 sum of a string
     * @param key The string to get the sum for
     * @return
     */
    public final String getMD5(String key) {
        String hexSig = null;

        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.reset();
            digest.update(key.getBytes());
            byte messageDigest[] = digest.digest();

            hexSig = toHexString(messageDigest);
        } catch (Exception e) {
        }

        return hexSig;
    }


    /**
     * Registers a TokBox user account, or throws an exception upon failure.
     * 
     * @param email
     * @param password
     * @param firstName
     * @param lastName
     * @param userName
     * @return
     * @throws com.soashable.tokbox.TokBox.TokBoxException Can be either regular or Internal exception.
     */
    public TokBoxAccount registerUser(String email, String password, String firstName, String lastName, String userName) throws TokBoxException {

        JSONObject retObj = call("registerUser", email, password, firstName, lastName, userName, null);
        
        String widget = ((JSONString)retObj.get("widgetid")).getValue();
        String userId = ((JSONString)retObj.get("userid")).getValue();

        TokBoxAccount a = new TokBoxAccount();
        a.setEmail(email);
        a.setUserId(userId);
        a.setWidget(widget);

        accountCache.put(email, a);

        return a;
    }

    /**
     * Get the widget ID for a given user, or throw Exception upon failure.
     * 
     * @param email
     * @param password
     * @return
     * @throws com.soashable.tokbox.TokBox.TokBoxException Can be either regular or Internal exception.
     */
    public String getWidget(String email, String password) throws TokBoxException {
        TokBoxAccount a;

        if( accountCache.containsKey(email) ) {
            a = accountCache.get(email);
        } else {
            a = new TokBoxAccount();
            accountCache.put(email, a);
        }            

        // make call and set widget if we don't have it
        if( a.getWidget() == null ) {
            JSONObject retObj = call("getWidgetID", email, password);
            String widget = ((JSONString)retObj.get("widgetid")).getValue();

            a.setWidget(widget);
        }
        
        return a.getWidget();
    }
    
    /**
     * Get a UserID given an email address
     * @param email
     * @param password
     * @return
     * @throws com.soashable.tokbox.TokBox.TokBoxException Can be either regular or Internal exception.
     */
    public String getUserId(String email, String password) throws TokBoxException {
        TokBoxAccount a;

        if( accountCache.containsKey(email) ) {
            a = accountCache.get(email);
        } else {
            a = new TokBoxAccount();
            accountCache.put(email, a);
        }            

        // make call and set widget if we don't have it
        if( a.getUserId() == null ) {
            JSONObject retObj = call("getUserID", email, password);
            String userId = ((JSONString)retObj.get("userid")).getValue();

            a.setUserId(userId);
        }

        return a.getUserId();
    }
    
    /**
     * Convenience method for the longer variation of call
     * @param function
     * @param email
     * @param password
     * @return
     * @throws com.soashable.tokbox.TokBox.TokBoxException Can be either regular or Internal exception.
     */
    protected JSONObject call( String function, String email, String password) throws TokBoxException {
        return call(function, email, password, EMPTY_PARAM, EMPTY_PARAM, EMPTY_PARAM, EMPTY_PARAM);
    }
    
    /**
     * The heart of the API, do all necessary encoding and decoding to a JSON
     * value. Returns a JSON object with return value, or throws an exception
     * if "success" was set to false.
     * 
     * @param function
     * @param email
     * @param password
     * @param firstName
     * @param lastName
     * @param userName
     * @param format
     * @return
     * @throws com.soashable.tokbox.TokBox.TokBoxException Can be either regular or Internal exception.
     */
    protected JSONObject call(String function, String email, String password, String firstName, String lastName, String userName, String format) throws TokBoxException {
        URL url;
        URLConnection urlConn;
        DataOutputStream printout;
        DataInputStream input;
        
        if( firstName == null ) { firstName = EMPTY_PARAM; }
        if( lastName == null ) { lastName = EMPTY_PARAM; }
        if( userName == null ) { userName = EMPTY_PARAM; }
        if( format == null ) { format = EMPTY_PARAM; }

        String hexPassword = null;
        String hexSig = null;
        String returnString = null;
        String callTime = String.valueOf(new java.util.Date().getTime());

        hexPassword = getMD5(password);

        String signature =
                apiKey +
                function +
                callTime +
                email +
                firstName +
                format +
                lastName +
                hexPassword +
                userName +
                "1.0" +
                apiSecret;

        hexSig = getMD5(signature);

        try {
            url = new URL(apiUrl);
            urlConn = url.openConnection();
            urlConn.setDoInput(true);
            urlConn.setDoOutput(true);
            urlConn.setUseCaches(false);
            printout = new DataOutputStream(urlConn.getOutputStream());

            String content =
                    "call="+function +
                    "&api_key=" + apiKey +
                    "&format=" + format +
                    "&call_id=" + callTime +
                    "&username=" + userName +
                    "&firstname=" + firstName +
                    "&lastname=" + lastName +
                    "&email=" + email +
                    "&password=" + hexPassword +
                    "&v=1.0" +
                    "&sig=" + hexSig;
            printout.writeBytes(content);
            printout.flush();
            printout.close();

            JSONParser jsonP = new JSONParser(urlConn.getInputStream());
            JSONObject ret = (JSONObject)jsonP.nextValue();
            
            // if the request was not successful
            if( !((JSONBoolean)ret.get("success")).getValue() ) {
                int errorCode = ((JSONInteger)ret.get("errorCode")).getValue().intValue();
                
                String message = null;
                if(ret.containsKey("message")) {
                    message = ((JSONString)ret.get("message")).getValue();
                } else {
                    message = "Unknown error.";
                }
                
                throw new TokBoxException(errorCode, message);
            }
            
            return ret;
        } catch (TokBoxException e) {
            throw e;
        } catch (Exception e) {
            throw new TokBoxInternalException("Something terrible happened.", e);
        }
    }
    
    /**
     * An exception based on success=false return values.
     */
    public class TokBoxException extends Exception {
        private int errorCode;

        public TokBoxException(int errorCode, String message, Exception cause) {
            super("[" + errorCode+"] " + message, cause);
            this.errorCode = errorCode;
        }
        
        public TokBoxException(int errorCode, String message) {
            this(errorCode, message, null);
        }
        
        public int getErrorCode() {
            return errorCode;
        }

        public void setErrorCode(int errorCode) {
            this.errorCode = errorCode;
        }
    }
    
    /**
     * Extended exception that uses the internal error code.
     */
    public class TokBoxInternalException extends TokBoxException {
        TokBoxInternalException(String message, Exception cause) {
            super(INTERNAL_ERROR, message, cause);
        }
    }
}

